侯捷cPP面向对象程序设计

基于对象:Object Based 面对的是单一class的设计。

面向对象:Object Oriented 面对的是多重classes的设计,涉及到类和类之间的关系。

课程中设计到两种不同类设计:没有指针(成员变量)的类和带指针(成员变量)的类设计。

头文件一般采用h结尾,源文件一般采用cpp,但是也不一定!(如stl很多没有后缀名)

头文件采用防御式声明,采用 #ifndef *** #define *** #endif,避免多次引用。

注意声明文件的内容顺序,一般是前置声明、类声明、类定义。(疑问,采用源文件进行类定义,与采用头文件进行类定义有哪些区别)

有的函数在类声明时在类的内部直接定义(直接内联),内联只是一种编译提示,是否真的内联取决于函数复杂程度和编译器实现。

构造函数的默认参数和初始化列表的使用,初始化列表很重要,和复制不同!能提高程序的初始化性能。

构造函数可以有多个重载。

如果将构造函数放在private区域,则该类不能在外部构造对象,一般配合设计模式使用,采用工厂模式来构造类,禁止直接构造类的时候使用。例如:定义一个类的静态函数getInstance,该函数返回一个静态的对象。

常量成员函数的意义很重要,一般不改变成员变量的函数都声明为常量函数,在函数声明后面添加 const。方便常量对象直接调用。

明白参数传递中传值和传引用的意义,传引用与传地址效率一样。在类对象的参数传递中尽可能采用传引用的方式,对于不修改类对象的参数传递尽可能采用常量引用。

返回值同样重视传值和传引用,此时注意局部变量考虑到其生命周期,在传引用时要尤其注意,不然会出现野指针。

对于友元函数,可以直接访问友元的私有成员变量。相同class的各个对象之间互为友元。

关于操作符重载,用于实现带有符号语义的函数,注意其语法要求。

对于返回引用的情况,参考对序列化输出和连加连减等操作。

明白操作符重载什么时候需要成员函数,什么时候需要非成员函数。

带指针(变量)的类设计

拷贝构造函数、拷贝函数和析构函数

三大函数。带指针的类设计一定要重视这三大函数,主要是涉及到危险的指针赋值操作。

堆和栈

栈对象(变量)在离开作用域时销毁,调用对象的析构函数。

静态栈对象,离开作用于还存在,在整个程序结束的时候析构。

全局对象的生命周期,比main函数早存在,在整个生命周期结束之后才结束。

堆对象(变量)控制权交给程序员,自己创建(new),自己负责销毁(delete),所以一定要注意指针的赋值(拷贝)操作,容易产生问题,在学习c++11的智能指针后尽可能多使用智能指针。

堆对象的生成,使用new,先分配堆空间,再调用构造函数。

new的动作分解:首先分配足够的内存空间,然后将内存进行转型操作,然后调用对象的构造函数

void* mem = operator new(sizeof(class));

pc = static_cast(mem);

pc->class::class(*);

堆对象的释放,使用delete,先调用析构函数,然后再释放堆内存。

class::~class(pointer); operatr delete(pointer)

在String的设计中,在析构函数中调用delete释放字符串。

候老师的重点内容,在new复数类(上一课例子),在调试模式下会多得到32个字节,中间是类的大小(两个double),后面还有四个字节,加上一头一尾的小cookie中,一共是8+(32+4)+(4+4)=52个,在vc下分配内存是16的倍数,为64个。为什么?在回收的时候顺利的回收。在relese模式下没有头尾的添加,但是有cookie,8+(4+4)=16。注意小cookie中的地址记录大小和最后一个位比特来表示是借出还是回收。字符串类(String,成员变量只有一个char指针,4比特),在调试模式下4+(32+4)+(4+4)=48,在relese模式下位4+(4+4)=12,变成16的倍数是16。

如果分配的是数组:array new对应array delete。class *p = new class[3];(8X3)+(32+4)+(4X2)+4,最后的加4是VC的做法,用一个整数来记录数组的长度,结果是72,调整到16的倍数为80,其余的模式以此类推。正确的搭配模式下调用array delete时,看到cookie,知道要删除的空间的大小,不会引起内存泄漏,但是会根据记录数组的区域,3次调用析构函数。如果array new不用array delete的话,只会调用一次析构函数,这样剩下的指针所指对象不会调用析构函数。感悟:透彻明了!

字符串String类的实现细节。

头文件中添加防卫式定义。

字符串里面的类属性,放数组不好因为事先不知道大小,一般放一个指针,大小根据放的内容动态分配;32位平台上一个指针4个byte。

big three 函数:拷贝构造函数(优先考虑传入引用,不修改变量的值,所以添加const),拷贝复制函数作为成员函数,在返回时传出引用。析构函数,释放开辟的堆内存。

注意String的获取成员变量m_data指针,返回const修辞的char指针。

拷贝赋值函数注意首先判断是否是自我赋值。

if(this==&str)return *this;

类模板函数模板

静态static

类里面的可以包含static函数和static成员变量

每个类对象包含各自的成员变量,一个成员函数要被多个对象调用,需要this指针。

对象中静态static成员不变量属于类,只存在一份。(多个对象共用)

类的静态函数没有this指针,所以不能访问对象里面的类成员,只能是处理静态数据。

静态类成员数据,一定要在类的外面设初值或者叫定义。 注意定义方法 type class::member = ...

调用static函数的方式有两种,一是通过对象调用,第二种是通过类来调用。

模板技术

template class name{};

编译器会根据不同的参数,生成不同的代码,所以使用类模板可能造成代码的膨胀。

了解模板函数的意义和作用处理。

c++中的算法大量使用模板。

命名空间

为避免命名冲突,使用命名空间。(比较简单)

命名空间可以分多段定义。

using namespace *;全开

using ::;指定打开

组合与继承探讨类与类之间的关系

复合 Composition (has-a)

(自己的理解)一个类包含(有)另一个类的对象。注意UML类图,采用实心菱形,箭头指向包含的对象,菱形指向包含别人对象的类。

适配器模式,一个类调用另外类已有的函数(接口),用来满足新类对接口和名称的要求。

从内存的角度来解释复合,层层包含。

构造函数之间的关系,container拥有component,外部的构造函数先调用内部的默认构造函数,即构造由内而外。container::container(...):compoent(){……}

外部的析构函数先执行自己,再调用内部的析构函数,析构由外而内。container::~container(...):{……^compoent();}

委托关系(Delegation)按引用的复合

一个类包含一个类的指针,UML类图使用空心的菱形代替复合中的实心菱形。Pimpl

用指针相连,生命周期就不一致。

例子中采用委托实现字符串的引用计数。copy on write

继承 (is-a)

一个类从另一个类继承部分属性和方法。

uml类图,空心三角形指向父类。

使用继承,传达一种信息,子类是一种(父类)

继承跟虚函数搭配最有价值–重载。

从内存的角度来看,子类的对象中有父类的成分。

构造由内而外,derived的构造函数首先调用base的默认构造函数。Derived::Derived(...):Base(){....};

析构由外而内,derived的析构函数先执行自己,然后才调用base的析构函数。Derived::~Derived(...){...~Base();};

父类的析构函数必须是虚函数,否则会出现不可预期的情况。

非虚函数,你不希望继承的类重新定义(覆盖override)它。

虚函数,你希望继承类重新定义(覆盖)它,而且你对它已经有默认定义。

纯虚函数,你希望继承类一定要重新定义它。virtual …… = 0;

子类对象调用父类的函数,父类的函数中采用虚函数,再调用子类重载的函数。父类中将关键动作延缓到子类中来实现,这种函数的做法叫做Template method,在框架中大量使用。

继承加复合关系下的构造和析构

子类从父类继承,子类还包含一个类的对象,构造函数先调用父类构造还是复合的对象?

父类包含一个复合对象,子类继承。应该先调用复合构造,父类构造和子类构造。

学习文件资源管理类中使用的 Composite 委托加继承的设计方法。

portotype 设计模式。现在创建未来的类。一个类包含一个静态对象,自身的对象,自己创造了自己。

静态成员变量一定记住在类的外面进行定义。

转换函数 conversion function

从一种类型转换成另外一种类型,相互转换。

定义转换函数:函数不可以有参数,没有返回参数。operator 转换类型() const {return 类型}

转换函数注意合理性。

non-explicit-one-argument actor

(一个实参就够了)非explicit的带一个实参的构造函数。从一个实参构建一个对象。可以把别的东西转换成对象。

转换函数和non-explicit-one-argument actor在一起的时候,会造成二义性,编译器会报错。

explicit-one-argument ctor 明确的一参数构造函数,不要不同类型的转换。explicit大部分用在构造函数的前面。

pointer-like classes 关于智能指针

像指针的类,比指针再多一些东西。

智能指针shared_ptr

封装了一个真正的指针,指针所允许的动作该类都支持。*和->的操作。T& operator*()const { return *px; } T* operator->()const { return px; }

一个符号作用在对象上就消耗掉了,->符号除外,得到的指针对象继续用箭头符号。

关于标准库STL的迭代器。另外一种类似于指针的类。

reference operator*()const { return (*node).data; }//reference 相当于T&

pointer operator->() const { return &(operator*());} //pointer 相当于T*

function-like classes 仿函数

函数的特点,函数名称,小括号()-函数调用操作符,可以接受一个小括号作为操作符,那么就可以成为function-like。

const T& operator()(const T& x) const {return x;}

一定会重载 ()操作。

标准库中,仿函数都会去继承奇特的base classes.

namespace 经验谈

尽可能使用命名空间,防止变量名和函数名的冲突。

class模板

template ……T抽象变量类型。

function模板

template函数定义

成员模板 member template

在模板类中存在一个新的模板,外面的模板是一个允许变化的东西,如果外部变化项确定,里面的变量又可以变化。

把两个继承类构成的pair放进一个有两个基类的pair中是可行的。反之不可以。

父类的指针可以指向子类的对象。up-cast。

智能指针模板为了实现up-cast,必须使用成员模板。

模板特化 specialization

泛化,在用的时候进行类型化。

设计模板之后,想绑定某种类型,就叫做特例化。指定了特定类型后编译器会根据参数选择相关代码。

partial specialization 偏特化

个数上的偏。模板有多个模板参数,对部分参数进行特例化

范围上的偏。从任意类型,特例化到指针这一种类型。

template temeplate parameter 模板模板参数

template class Container> class XCls{private: Container c;……}

XCls mylst1; 错误

template using Lst=list>>; XCls mylst2; 正确

关于c++标准库

数据结构容器和算法。

多使用标准库,写小例子测试标准库。

测试是否支持c++11,cout<<__cplausplus <<endl;

三个主题(标准库中的新语法)

数量不定的模板参数

auto关键字。auto自动确定变量类型。

ranged-base for for(decl: coll){statement},注意传值和传引用。for(auto& elem: vec){elem*=3;}

对象模型关于vptr和vtbl

虚指针和虚表,一个类的对象内存占用什么样的内存?当一个类有虚函数的时候,对象里面就会多一个指针。一个虚函数和一万个虚函数是一样的。

继承会把成员变量继承也会把函数继承下来。

一般的函数和虚函数区别。

虚拟表中存放的都是指针,虚函数指针。

编译器看见调用虚函数时,采用动态绑定。通过虚指针,查看虚表,再看调用的是哪一个函数。(普通函数调用采用动态绑定)(* p->vptr[n])(p);编译器会找到n的位置编号。

静态绑定 call cll……

动态绑定,条件1、通过指针调用;2、指针是向上转型 up case;3、调用的是虚拟函数。(多态)

关于this

模板方法,this指针的使用场景。会把当前对象当做this指针传到方法里面。一个父类的方法A里面调用了一个虚函数,这个虚函数在子类中重载,这样当子类调用父类的方法A时,会通过父类的A函数,调用子类重载过的虚函数。

所有的成员函数都隐藏了一个this参数。

关于 Dynamic Binding

非指针调用不会产生动态绑定。

通过指针找到虚指针,找到虚表,找到相应的函数地址。

关于const

当成员函数的const和non-const版本同事存在是,const object只会(只能)调用const版本,non-const object只会(只能)调用non-const版本。

const object 调用const 成员函数可行,但是non-const成员函数不可行。

non-const object 可以调用 const 成员函数,non-const 成员函数。

non-const 成员函数可以调用const 成员函数,反之则不行。

关于New 和 Delete

new 先分配 memory,再调用ctor。

delete 先调用dtor,再释放memory。

array new,一定要搭配 array delete。

重载 new 和 delete 全局函数(编译器调用)

重载全局 ::new ::new[] ::delete ::delete[]

inline void* operator new(size_t_size){……分配内存}

inline void operatpr delete(void* ptr){……释放内存}

上面的重载函数不能放在namespace中,是全局的函数,影响是非常大的。

重载成员函数 new 和 delete

成员函数 void* operator new(size_t);

成员函数 void operator delete(void*, size_t);

成员函数 void* operator new[](size_t);

成员函数 void operator delete[](void*, size_t);

调用函数时添加了::,调用全局的函数,绕过类所定义的new和 delete版本。

关于new 和new[]参数的大小。有虚函数的对象对多一个指针的大小4。

对象数组[],对多一个4字节的区域,记录数据的大小是多少。

我们可以重载类成员的operator new(),写出多个版本,前提是每个版本的声明都必须有独特的参数列,其中第一个参数必须是size_t,其余参数以new 所指定的placement arguments为初值。出现于new(……)小括号内的便是所谓placement arguments。

我们也可以重载类成员operator delete(),写出多个版本,但是绝对不会被delete调用。只有当new所调用的ctor抛出异常,才会调用这些重载的函数operator delete()。它们只能这样被调用,主要用来归还还未完全创造成功的object所占用的memory。即使operatordelete(……)未能一一对应operator new(……)。也不会出现任何报错。

标准库中String使用 new(extra)扩展

string采用new(extra)进行自己的内存分配,用于实现特定内存结构中引用计数的处理。

引用 reference

int x = 0; int& r = x; sizeof(r) == sizeof(x); &x = &r;

object和其引用的大小相同,地址也相同(全都是假象),java里面的变量都是引用。

编译器实现都是使用指针来实现引用,但是在使用时可以从逻辑上把引用当做原值来使用。

声明引用的时候一定要有初值。设置完以后不能再变化。指针可以变化。

引用的地址和原始类型的地址相同

引用通常不用再变量的声明,引用主要用于参数类型(传参数)和返回类型(返回参数)的描述。

const是不是函数签名的一部分?是

析构和构造函数

继承关系下的构造和析构

继承类的构造函数首先调用父类的默认构造函数再执行自己。

继承类的析构函数先执行自己再执行父类的析构函数。

复合关系下的构造和析构

拥有者的构造函数先调用组件的默认构造函数,然后再执行自己。

拥有者析构函数先执行自己,然后才地哦啊用组件的析构函数。

继承加复合关系下的构造和析构

继承类的构造函数先执行基类的默认构造函数,然后调用组件的默认构造函数,然后再执行自己。Derived::Derived(……):Base(), Component(){};

继承类的析构函数首先执行自己,然后调用组件的析构函数,然后调用基类的析构函数。Derived::~Derived(……){……~Component(), ~BAse()}

本页共201段,9302个字符,19027 Byte(字节)